Monads are foundational in functional programming. In recent years, C# as a programming language has embraced functional features such as pattern matching, LINQ, and lambda expressions. The evolution of this language from classic OOP to a more functional paradigm is definitely worth exploring via elevated data types like monads. In this take, I will explore functional monads in C# via a REST API.
The implementation will adhere to functional programming principles like immutability and expression-based code. This means the entire business logic uses expressions instead of imperative constructs like the conditional if statement. The code remains fluent, so it is possible to dot into multiple expressions without much effort. In the end, I hope you see a new way of looking at problems while remaining right at home and using the already familiar C# type system.
I will assume a firm grasp of generics, OOP principles like inheritance and encapsulation, and delegates. A good solid understanding of the delegate type Func<>
, for example, will come in handy. If all this is new to you, I recommend learning those concepts first before diving into functional programming in C#.
The API attempts to solve a real-world banking solution with error handling, retries, and account validation. Of course, no demo code will ever match an actual solution that serves real customers, but this comes close enough to prove the paradigm works and is worth looking into.
Feel free to check out the sample code, which is up on GitHub. I highly encourage cloning the repo, running unit tests, setting a breakpoint, and running the code. It is the best way to pick up new C# skills.
Go ahead and download the latest .NET 5 version if you don’t have this already because this uses the latest C# 9 features. The sample code uses a MongoDB backend to persist account data. MongoDB is lightweight, and a full install only takes up about a few hundred megabytes, so I recommend having an instance running on local. There isn’t enough room here to put a step-by-step guide on how to set up MongoDB, but there are plenty of guides on their website to get a windows service up and running, assuming it runs on Windows 10.
I will reference the code on GitHub quite heavily and will not attempt to make this a tutorial since there is too much code. My goal is to show you how monads work in the real world, not how to set up a project and put using statements. There will be chunks of code that will not be shown so you are encouraged to explore the code further in the GitHub repo.
These are the dependencies added to the project:
- MongoDB.Driver: talks to MongoDB via a fluent API; I put a wrapper around this to make it testable
- LanguageExt.Core: C# language extension methods and prelude classes that unleash monads; I opted for the beta version because this has some really cool features like automatic retries
- MediatR: mediator implementation in .NET with query-command segregation; fits the functional paradigm quite nicely
What are monads?
Monads elevate basic C# types and unlock functional patterns. This is what allows doting into expressions while remaining inside the abstraction. Think of monadic types as containers that hold any generic type. As long as the container holds the data type, it is possible to chain as many expressions as necessary.
The following unit test illustrates:
1 2 3 4 5 6 7 |
void Bind() => Assert.Equal( 5, Optional(1) .Bind<int>(o => o + 2) .Bind<int>(o => o + 2) ); |
The Bind<>
method is chainable via a dot and is a must in monads because every monad is composable via Bind. The chain of expressions drives the logic, and everything stays within the abstraction. Optional
returns Option<int>
, a monadic type containing a basic int, and it is chainable for as long as possible. Monads must also have a way to return the contained type so notice how the test compares the result with a plain int type of 5. The delegate parameter in Bind is of type Func<A, Option<B>>
, meaning it takes in a generic type A already in the container and outputs another monad that contains type B.
The LanguageExt.Core
library has static methods like Optional via a prelude static using statement. Bringing in the static LanguageExt.Prelude
namespace allows any monadic type to be initialized.
A more generic but less powerful pattern is a functor. Monads must have a Bind, and functors must have a Map
.
This is how to break out of the monad container via a functor:
1 2 3 4 5 6 |
void Map() => Assert.Equal( "1", Optional(1) .Map(o => o.ToString(CultureInfo.InvariantCulture)) ); |
Think of a Map like Select in LINQ. The example calls a delegate that returns a projection of the contained data type while unwrapping the monad. In this call, the delegate type is Func<int, string>
and does not return an elevated data type.
The Option monad is a great place to start learning about functional programming because it encapsulates a ubiquitous programming challenge: whether a type is set to Some<A>
or None
. Functional programming principles eliminate null, so instead of returning null, it must return a value, in this case, None
. This container deals automatically with default or null values and is one reason why it is good to stay within the elevated type for as long as possible.
Classic OOP, technically, has been lying this whole time with APIs that return null. Strictly speaking, a null is not an object but the lack of an object. Returning null is like selling street mangos by making them optional and then telling consumers you never intended to carry any! This is an area where OOP and the functional paradigm start to merge to come up with honest APIs that are predictable and have valid return types.
An interesting behavior with monadic data types is Bind
which allows chaining even when everything returns None
:
1 2 3 4 5 6 7 8 |
void BindBehavior() => Assert.Equal( None, Optional(1) .Bind<int>(o => o + 2) .Bind<int>(_ => None) .Bind<int>(_ => 1) ); |
As soon as the logic shorts, it escapes the chain of expressions and simply returns None. This allows fluent code that reads much like plain English, and everything after None does not even execute. This keeps the API flow honest because it is a contradiction to have None and then Some simultaneously.
In contrast, the applicative pattern does the complete opposite because it runs everything and collects the results. This pattern fits perfectly in validation logic that needs to sum up a list of errors. The Apply
functional pattern is less generic but more powerful than a functor via Map
and less powerful than a monad via Bind
.
Containers can be boxes inside other boxes
The following illustrates:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
void ApplyValidationFail() => Assert.Equal( "[fail_1, fail_2]", ( Success<string, Option<int>>(3), Fail<string, Option<int>>("fail_1"), Success<string, Option<int>>(3), Fail<string, Option<int>>("fail_2") ) .Apply((a, b, c, d) => from w in a from x in b from y in c from z in d select w + x + y + z) .FailToSeq() .ToString() ); |
Be sure to bring in the static namespace LanguageExt.Prelude
so Option
, Success
, and Fail
are available. The Apply
collects all validation results found in a, b, c, and d. Then, it sums up the result via a chain of monads using Bind. LINQ makes this code more readable with from … select
. If this is surprising, it is because monads can declare a SelectMany<B, C>
method that does both the bind and subsequent projection using functional composition. What’s nice is that this LINQ statement has the exact same behavior as the example shown earlier. If anything shorts the logic via None
, it quits early and does not continue execution. In this example, the validation collects errors in FailToSeq
and turns the array into a string.
To show how both the monadic and applicative patterns behave, look at two examples where one fails, and one succeeds:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
void ApplyValidationNone() => Assert.Equal( None, ( Success<string, Option<int>>(3), Fail<string, Option<int>>("fail_1"), Success<string, Option<int>>(3), Fail<string, Option<int>>("fail_2") ) .Apply((a, b, c, d) => from w in a from x in b from y in c from z in d select w + x + y + z) .Match( Succ: o => o, Fail: _ => None ) ); void ApplyValidationSome() => Assert.Equal( 8, ( Success<string, Option<int>>(3), Success<string, Option<int>>(1), Success<string, Option<int>>(3), Success<string, Option<int>>(1) ) .Apply((a, b, c, d) => from w in a from x in b from y in c from z in d select w + x + y + z) .Match( Succ: o => o, Fail: _ => None ) ); |
Ultimately, what drives the logic isn’t the validation container defined as Validation<string, Option<int>>
but the Match
at the end of the chain of expressions. A Match essentially acts like a fork and only executes a single path depending on the state of the container. If validation passes, it sends back the Option<int>
. If not, it returns None. This is a nice way to unwrap the monadic type and simply return a value when the chain of expressions is done.
The last point I want to make is this, look at the validation type once more. It cares about two things, the failure, which is a list of strings, and the valid result which is another monad. Yes, much like Russian nesting dolls, containers can have other containers inside as long as the data type closely models the logic.
In functional programming, smart data types over dumb algorithms are what drives the main logic, as opposed to OOP, where smart algorithms with dumb or anemic types do most of the work. This is one key differentiator because imperative code thrives with dumb types via if statements and for-loops, whereas expressions need to dot into a chain of objects that are much more capable. This makes Bind so powerful because it can compose a long chain of expressions that encapsulate the bulk of the logic.
Enough simple theory with makeshift monadic types, it’s time to throw these same patterns and techniques into a real API.
Grabbing monadic accounts off the database
I opted to use a real database like MongoDB, instead of an in-memory one, because I can then throw asynchrony and error handling at this. Gladly enough, there is a monad in LanguageExt
that encapsulates all these requirements: TryOptionAsync
. This has the already familiar None and Some, has a Fail state, and it is awaitable like Task<>
.
This capability is exactly what I mean by smart data types; you start with the problem at hand then pluck the type that models the solution most closely. This TryOptionAsync<A>
container encapsulates everything necessary to build a repository that has a bank account state with plenty of niceties.
If you ever find yourself in a bind, so to speak, it is best to step back and examine the type because it might be working against you.
To get a single account from the database via a monad:
1 2 3 4 |
TryOptionAsync<AccountState> GetAccountState(Guid id) => TryOptionAsyncExtensions.RetryBackOff( TryOptionAsync(() => _stateDb.FindAsync(a => a.AccountId == id)), 100); |
The RetryBackOff
is a nice extra feature with the beta version of this library. It attempts to fire the lambda expression inside TryOptionAsync<AccountState>
, and if this fails, it automatically retries. The default setting is 3 retries before bombing out and returning a Fail state. This supports backing off, so the next retry time is doubled, for example, 100, 200, and 400 milliseconds. Even if the MongoDB service is down, it will timeout at the default 30 seconds connection timeout; it will then attempt to reconnect at least thrice.
Because functional programming has data types that are immutable, they have no side effects. Automatic retries is an added feature that naturally flows from being side-effect free, which allows building robust solutions with high availability.
Asynchrony is another bit of complexity that gets abstracted away with this monad. For the most part, async/await will no longer need to be sprinkled all over the code. The monadic type also implements async correctly via ConfigureAwait(false)
and whatnot, so this is doing a lot of the heavy lifting within the container.
I put the database query inside a wrapper so it is testable. This enables unit testing of the nomadic type that comes out of the db repo:
1 2 3 4 5 6 7 8 9 10 11 12 |
async Task GetAccountState() { _stateDb .Setup(m => m.FindAsync( It.IsAny<Expression<Func<AccountState, bool>>>(), null)) .ReturnsAsync(AccountState.New(Guid.NewGuid())); var result = await _repo.GetAccountState(Guid.NewGuid()).Try(); // Try Assert.True(result.IsSome); } |
The logic in the system under test, which is the account repo, is almost trivial. The point here is that monads work well with mocks too. The result that comes out of the repository is a monadic type that supports the property IsSome
to verify the container has at least account info in it. Take a close look at Try
because this unwraps the container and returns an OptionalResult<AccountState>
that has Some or None. The Try expression encapsulates a try/catch and handles errors automatically.
I recommend exploring the rest of this GET endpoint in the GitHub repo. Start in Bank.Data
and if you are so bold bust open the GetAccountByIdHandler
in Bank.App.Queries
. There is even validation to guarantee the id, which is a string in the request, is always a legit Guid.
HTTP status codes with monads
The AccountResponse
is a namespace alias that encapsulates the monadic container. This is the data type that has the state of the overall API HTTP response.
1 2 3 |
using AccountResponse = LanguageExt.Validation< Bank.App.Validators.ErrorMsg, LanguageExt.TryOptionAsync<Bank.App.Accounts.AccountViewModel>>; |
Much like shown earlier with nested types inside a validation container. This elevated type has ErrorMsg
to capture a failure, and the view model wrapped around the TryOptionAsync
.
Turns out, this very same validation container is useful when handling HTTP responses:
1 2 3 4 5 6 7 8 9 10 11 12 |
Task<IActionResult> ToActionResult<T>( this Validation<ErrorMsg, TryOptionAsync<T>> self) => self.Match( Fail: e => Task.FromResult<IActionResult>( new BadRequestObjectResult(e)), Succ: valid => valid.Match<T, IActionResult>( Some: r => new OkObjectResult(r), None: () => new NotFoundResult(), Fail: _ => new StatusCodeResult( StatusCodes.Status500InternalServerError) ) ); |
The Match drives the response logic and only executes a single path depending on the state of the container. Match also unwraps the monad container and returns Task<IActionResult>
which is what the ASP.NET controller wants.
If validation fails, it is a 400 Bad Request response. If there are errors while talking to MongoDB, it is a 500 Internal Server Error. Also, if no account data was found, a 404. Because the elevated type models the problem so well, the code solution appears simple on the surface. Therefore, it is always a good idea to put more thought into design than cranking out a code solution that requires a lot of complexity and elbow grease.
Because ToActionResult
is an extension method, the unit test becomes even easier to write without crazy mocks or anything:
1 2 3 4 5 6 7 8 |
async Task ToActionResultOk() => Assert.NotNull( await Success<ErrorMsg, TryOptionAsync<AccountViewModel>>( TryOptionAsync(AccountViewModel.New( AccountState.New(Guid.NewGuid())))) .ToActionResult() as OkObjectResult ); |
Here I only test the happy path, but you can verify every nook and cranny. A more complete suite of tests can be found in the GitHub repo.
A bit of pattern matching
The POST endpoint takes in an AccountTransaction
with a transaction event that makes a mutation to the account. This will do an upsert to the account state and keep an account event trail to track individual transactions.
A switch expression is useful to figure out where the logic splits:
1 2 3 4 5 6 7 8 9 10 |
Task<AccountResponse> Handle( AccountTransaction request, CancellationToken cancellationToken) => request.Event switch { TransactionEvent.CreatedAccount => CreatedAccount(request), TransactionEvent.DebitedFee => DebitedFee(request), TransactionEvent.DepositedCash => DepositedCash(request), _ => InvalidTransactionEvent() }; |
This app currently supports account creation, deposits, and withdrawals. Any unsupported transactions automatically fail, and nothing else executes.
Account creation is somewhat straightforward because it only validates the account id, and account currency since those must be valid. There is an extension method AccountMustNotExist
that fails the request on already existing accounts. Open up AccountTransactionHandler
under Bank.App.Accounts.Commands
to take a peek.
The most complex transaction is the DebitedFee
because it has more validations:
1 2 3 4 5 6 7 8 9 10 |
Task<AccountResponse> DebitedFee(AccountTransaction request) => (IsValidGuid(request.AccountId), request.AmountMustBeSet()) .Apply((id, trans) => _repo.GetAccountState(id).AccountMustExist() .Bind(acc => acc.HasEnoughFunds(trans.Amount)) // current state -> acc .Bind<TryOptionAsync<AccountViewModel>>(acc => PersistAccountInfo( acc.Debit(trans.Amount), trans.ToDebitedEvent()))) .Bind(resp => resp) .AsTask(); |
The logic remains fluent while simply dotting into the next expression. The Apply
acts on a tuple with a list of validations that run even if any fail. The applicative pattern guarantees that all validations execute without quitting early and collects a list of failures.
First, validate the request
. If valid, get the account state off the repo using a legit id
and validate that it exists in the database via AccountMustExist
. Remember the Bind behavior when there is a failure somewhere up in the chain? That’s right, not having enough funds in HasEnoughFunds
fails the transaction, and nothing else happens.
Then, dot into persisting the new state and unwrap then wrap it around a Task<>
. Because this validates in two places, the unwrapping at the end via Bind nukes the extra Validation<ErrorMsg, AccountResponse>
container to just AccountResponse
, which already has a Validation
. The AsTask
is necessary because the mediator implementation requires a Task even though the monad is already async. A bit of a wrinkle when dealing with containers with boxes inside other boxes but expressions keep the unwrapping code minimal.
Persist AccountState and AccountEvent
The PersistAccountInfo
grabs the current account state of the database in acc
, takes a valid request via trans
that comes out of the validated request, and a pplies the mutation that returns a new state and stores both account state and the event in the database.
The following implements this:
1 2 3 4 5 6 |
TryOptionAsync<AccountViewModel> PersistAccountInfo( AccountState state, AccountEvent accountEvent) => from st in _repo.UpsertAccountState(state) // new state -> st from _ in _repo.AddAccountEvent(accountEvent) select AccountViewModel.New(st); |
The from … select
comes in handy to chain these expressions via Bind. If the upsert fails for any reason, the event is never recorded. The view model in the HTTP response comes from the select because it has the latest persisted state in MongoDB.
Once the state mutates, and it is successfully persisted, there is a way to guarantee an account event gets recorded via a retry:
1 2 3 4 5 6 |
TryOptionAsync<Unit> AddAccountEvent(AccountEvent accountEvent) => TryOptionAsyncExtensions.RetryBackOff( TryOptionAsync(() => _eventDb.InsertOneAsync(accountEvent).ToUnit()), 100, 5); |
Much like a FedEx truck that guarantees delivery of the package, this is identical to the code shown earlier when it grabs the account in the GET endpoint with one small difference. Instead of returning void, return Unit
. Functional programming must have at least one return value. A Unit is basically a discard which is exactly what the from
in LINQ did with this using an underscore.
There are other alternatives to guarantee an event gets recorded, such as dropping messages in a queue or wrapping both calls in a single db transaction, but monadic retries felt like a good enough approach.
To test GET/POST
endpoints via curl, run the Banking app (Bank.Web project), then check out the readme in the GitHub repository. Here, I will show some quick tests:
Functional monads in C#
C# has come a long way from its humble beginnings since its first release, which was heavily biased towards OOP. The elegant type-system, generics, lambda expressions, pattern matching, and LINQ turn this general-purpose language into a functional programming juggernaut.
So far, I have been pleasantly surprised at the level of ease with which monads blend into the language. Of course, these patterns and ideas are still experimental, but my hope is it won’t take long for these techniques to be incorporated in .NET proper.
If you liked this article, you might also like Build a REST API in .NET Core.
Load comments